package com.egrand.sweetapi.core.script.impl;

import com.egrand.sweetapi.core.script.DynamicScriptContext;
import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;

import javax.script.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * JavaScript脚本上下文
 */
public class JsDynamicScriptContext implements DynamicScriptContext {

    private static final String EXPRESSION_PREFIX = "/* generated by execute expression */ return ";

    /**
     * 脚本引擎
     */
    private ScriptEngine jsEngine;

    /**
     * 全局变量绑定
      */
    private Bindings globalBindings;

    /**
     * 局部变量绑定
     */
    private Bindings engineBindings;

    /**
     * 脚本名称
     */
    private String scriptName;

    public JsDynamicScriptContext() {
        this.jsEngine = GraalJSScriptEngine.create(null,
                Context.newBuilder("js")
                        .allowAllAccess(true)
                        .allowHostAccess(HostAccess.newBuilder(HostAccess.ALL)
                                /// Highest Precedence
                                /// accepts is null, so we can quickly assert the type

                                // Goal: [] -> Object
                                // When the target type is "exactly" <java.lang.Object> and the source is a array like object (json array for
                                // example) then we want it to be a <java.util.List>, otherwise the default behavior of graal is <java.util.Map>
                                .targetTypeMapping(
                                        List.class,
                                        Object.class,
                                        Objects::nonNull,
                                        v -> v,
                                        HostAccess.TargetMappingPrecedence.HIGHEST)
                                .build())
                        .allowHostClassLookup(s -> true));
        jsEngine.getContext().setBindings(new SimpleBindings(), ScriptContext.GLOBAL_SCOPE);
        this.globalBindings = this.jsEngine.getBindings(ScriptContext.GLOBAL_SCOPE);
        this.engineBindings = this.jsEngine.getBindings(ScriptContext.ENGINE_SCOPE);
    }

    public JsDynamicScriptContext(Map<String, Object> variables) {
        this();
        this.putMapIntoContext(variables);
    }

    @Override
    public String getScriptName() {
        return this.scriptName;
    }

    @Override
    public void setScriptName(String scriptName) {
        this.scriptName = scriptName;
    }

    @Override
    public String getString(String name) {
        return Objects.toString(get(name), null);
    }

    @Override
    public Object get(String name) {
        return this.globalBindings.get(name);
    }

    @Override
    public DynamicScriptContext set(String name, Object value) {
        this.globalBindings.put(name, value);
        return this;
    }

    @Override
    public Map<String, Object> getRootVariables() {
        return this.globalBindings;
    }

    @Override
    public void putMapIntoContext(Map<String, Object> map) {
        if (map != null && !map.isEmpty()) {
            this.globalBindings.putAll(map);
        }
    }

    @Override
    public void addImport(String packageName) {

    }

    @Override
    public Class<?> getImportClass(String simpleClassName) {
        return null;
    }

    @Override
    public Object execute(String script) throws ScriptException {
        script = "(function(){" + script + "})();";
        return this.jsEngine.eval(script);
    }

    @Override
    public Object executeExpression(String script) throws ScriptException {
        return this.execute(EXPRESSION_PREFIX + script);
    }
}
